import 'dart:async';
import 'dart:isolate';

import 'ports.dart';
import 'runner.dart';
import 'util.dart';

const int _shutdown = 0;
const int _run = 1;

class IsolateRunner implements Runner {
  late final Isolate isolate;
  late final SendPort _commandPort;
  Future<void>? _onExitFuture;
  Future<void>? _closeFuture;

  IsolateRunner(this.isolate, SendPort commandPort)
      : _commandPort = commandPort;

  static Future<IsolateRunner> spawn() async {
    var channel = SingleResponseChannel();
    var isolate =
        await Isolate.spawn(IsolateRunnerRemote._create, channel.port);
    isolate.setErrorsFatal(false);
    var pingChannel = SingleResponseChannel();
    isolate.ping(pingChannel.port);
    var commandPort = await channel.result as SendPort;
    var result = IsolateRunner(isolate, commandPort);
    await pingChannel.result;
    return result;
  }

  @override
  Future<void> close() {
    var closeFuture = _closeFuture;
    if (closeFuture != null) {
      return closeFuture;
    }
    var channel = SingleResponseChannel();
    _commandPort.send(list2(_shutdown, channel.port));
    return _closeFuture = channel.result.then(ignore);
  }

  @override
  Future<R> run<R, P>(FutureOr<R> Function(P argument) function, P argument,
      {Duration? timeout, FutureOr<R> Function()? onTimeout}) {
    return singleResultFuture<R>((SendPort port) {
      _commandPort.send(list4(_run, function, argument, port));
    }, timeout: timeout, onTimeout: onTimeout);
  }
}

class IsolateRunnerRemote {
  final RawReceivePort _commandPort = RawReceivePort();

  IsolateRunnerRemote() {
    _commandPort.handler = _handleCommand;
  }

  SendPort get commandPort => _commandPort.sendPort;

  static void _create(Object? data) {
    var initPort = data as SendPort;
    var remote = IsolateRunnerRemote();
    initPort.send(remote.commandPort);
  }

  void _handleCommand(List<Object?> command) {
    switch (command[0]) {
      case _shutdown:
        _commandPort.close();
        (command[1] as SendPort?)?.send(null);
        break;
      case _run:
        var function = command[1] as Function;
        var argument = command[2];
        var responsePort = command[3] as SendPort;
        sendFutureResult(Future.sync(() => function(argument)), responsePort);
        break;
    }
  }
}
